package com.acme.bankapp.service.email;

import com.acme.bankapp.util.ConcurrentQueue;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Email service uses concurrent blocking queue and Producer &lt;-&gt; Consumer pattern to work with emails.<br/>
 * Implements AutoClosable {@link #close()}
 * We got two pools:<br/>
 * 1) sending executors: pool of looping executors {@link ContinuousEmailSender}, working with {@link EmailTransport},
 * continuously trying to pop email from concurrent queue <br/>
 * 2) enqueuing executors. Putting emails to queue isn't done in main thread due to performance issues. <br/>
 * When you call{@link #enqueueEmail(Email)} new Task {@link EmailEnqueuer} is submitted to pool.
 * @author Gregory Sobko
 */
public class EmailService implements AutoCloseable{
    private EmailTransport emailTransport;
    private ExecutorService sendingExecutors;
    private ExecutorService enqueuingExecutors;
    private ConcurrentQueue<Email> queue;

    /**
     * Creates instance of EmailService.
     * Number of threads in pools is hardcoded in constructor
     * @param emailTransport transport to send mails
     * @param concurrentQueue implementation of queue
     */
    public EmailService(EmailTransport emailTransport, ConcurrentQueue<Email> concurrentQueue) {
        queue = concurrentQueue;
        this.emailTransport = emailTransport;
        final int QWorkerNum = 5;
        sendingExecutors = Executors.newFixedThreadPool(QWorkerNum);
        enqueuingExecutors = Executors.newSingleThreadExecutor();
        for (int i = 0; i < QWorkerNum; i++) {
            sendingExecutors.submit(new ContinuousEmailSender());
        }

    }

    /**
     * puts email to queue
     * @param email email to send
     */
    public void enqueueEmail(final Email email){
       // No need for additional check for enqueuingExecutors.isShutdown, because exception will be thrown
       enqueuingExecutors.submit(new EmailEnqueuer(email));
    }

    /**
     * Closing both pools (enqueuers and senders) in next steps:<br/>
     * 1) closing enqueuers pool from adding new tasks<br/>
     * 2) checking enqueuers finalized, if not =&gt; try to force terminate (interrupt) them on timeout <br/>
     * 3) close concurrent queue <br/>
     * 4) close senders queue
     */
    public void close(){
        // Disable new enqueuing by shutting down enqueuingExecutors pool
        enqueuingExecutors.shutdown();
        // Wait for sending executors to finish tasks
        try{
            if (!enqueuingExecutors.awaitTermination(15, TimeUnit.SECONDS)) {
                int numNotSent = enqueuingExecutors.shutdownNow().size();
                if(numNotSent>0){
                    System.err.println("Some email was not sent, resulting data loss! Number: " + numNotSent);
                }
                if (!enqueuingExecutors.awaitTermination(10, TimeUnit.SECONDS))
                    System.err.println("enqueuingExecutors did not terminate");
            }
        } catch (InterruptedException e) {
            enqueuingExecutors.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }finally {
            // Ok! now we finally close our concurrentQ
            queue.close();
            sendingExecutors.shutdown();
            try {
                // Wait a while for existing tasks to terminate
                if (!sendingExecutors.awaitTermination(15, TimeUnit.SECONDS)) {
                   sendingExecutors.shutdownNow(); // Cancel currently executing tasks

                    if (!sendingExecutors.awaitTermination(10, TimeUnit.SECONDS))
                        System.err.println("sendingExecutors did not terminate, trying another attempt");
                        sendingExecutors.shutdownNow();
                    }
                } catch (InterruptedException ie) {
                    sendingExecutors.shutdownNow();
                    Thread.currentThread().interrupt();
            }
        }
    }

    private class ContinuousEmailSender implements Runnable{
        @Override
        public void run() {
            try {
                while (true){
                    if(Thread.currentThread().isInterrupted()){
                        throw new InterruptedException();
                    }
                    Email  email = queue.pop();
                    EmailService.this.emailTransport.sendEmail(email);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Email email;
                while ((email = queue.tryPop())!=null){
                    try {
                        EmailService.this.emailTransport.sendEmail(email);
                    } catch (InterruptedException e1) {
                        System.err.println("oh shi! we were interrupted while afterwork! " +
                                "This will result in unsendeded data");
                        break;
                    }
                }
            }

        }
    }

    private class EmailEnqueuer implements Runnable{

        private Email email;

        private EmailEnqueuer(Email email) {
            this.email = email;
        }

        @Override
        public void run() {
            try {
                queue.push(email);
            } catch (InterruptedException | IllegalStateException e) {
                //e.printStackTrace();
                System.out.println("Mail was not sent! "+ e.getMessage());
            }

        }
    }

}
